package network.server;

import network.FileConnection;
import network.Message;
import network.SocketConnection;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Date;
import java.util.List;

import static java.lang.Integer.min;

public class ServerSkeleton implements AutoCloseable {
    private ServerSocket serverSocket;
    private RoomsPool sockets;
    private MessageQueue messageQueue;
    private int threadCount = 0;
    private Object threadCountMonitor = new Object();
    private final int maxThreadCount = 1000;

    private class MessageQueue {
        private List<Message> messageQueue;
        private boolean isInterrupted;

        private MessageQueue(List<Message> messageQueue) {
            this.messageQueue = messageQueue;
        }

        public void add(Message message) {
            synchronized (messageQueue) {
                if (!isInterrupted) {
                    messageQueue.add(message);
                    messageQueue.notify();
                }
            }
        }

        public Message remove() {
            synchronized (messageQueue) {
                if (!isInterrupted) {
                    if (messageQueue.isEmpty()) {
                        try {
                            messageQueue.wait();
                        } catch (InterruptedException e) {
                            return null;
                        }
                    }
                    return messageQueue.remove(0);
                }
                return null;
            }
        }

        public void close() {
            synchronized (messageQueue) {
                isInterrupted = true;
            }
        }
    }

    public ServerSkeleton(ServerSocket serverSocket, RoomsPool sockets, List<Message> messageQueue) {
        this.serverSocket = serverSocket;
        this.sockets = sockets;
        this.messageQueue = new MessageQueue(messageQueue);
    }

    public void listenSocket() throws InterruptedException {
        // Thread for clients accept
        new Thread(() -> {
            Socket socket;
            while (true) {
                try {
                    socket = serverSocket.accept();
                    System.out.println("Connection established");
                    synchronized (threadCountMonitor) {
                        if (threadCount + 1 > maxThreadCount) {
                            System.err.println("Could not initialize connection: there is no enough sources for thread creation");
                            socket.close();
                            continue;
                        }
                        threadCount++;
                    }
                    initializeNewConnection(socket);
                } catch (IOException e) {
                    System.err.println("Connection failed");
                    break;
                }
            }
        }).start();

        // Thread for messages sending
        new Thread(() -> {
            try {
                sendMessageToAllClients();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        Thread.currentThread().join();
    }

    public void initializeNewConnection(Socket socket) throws IOException {
        ClientSocket socketConnection = new ClientSocket(socket);
        new Thread(() -> {
            try {
                Message message = null;
                while (true) {
                    try {
                        message = readSocket(socketConnection);
                        if (!message.isValid()) {
                            Message answer = new Message("Admin", message.getSender(), "/snd", "Your message is not valid!", new Date());
                            answer.setRoom(message.getRoom());
                            socketConnection.write(answer);
                            continue;
                        }
//                        System.out.println("read: " + message);
                        switch (message.getKey()) {
                            case "/snd": {
                                message.setDate(new Date());
                                messageQueue.add(message);
                                //System.out.println("Message added to queue: " + message.getMessage());
                                break;
                            }
                            case "/chid": {
                                if (addSocket(socketConnection, message)) {
                                    socketConnection.setLogin(message.getRecipient());
                                }
                                break;
                            }
                            case "/hist": {
                                StringBuilder history = new StringBuilder(FileConnection.getHistory());
                                while (history.length() != 0) {
                                    int index = min(Message.maxMessageLength, history.length());
                                    String msg = history.substring(0, index);
                                    history.delete(0, index);
                                    socketConnection.write(new Message("Admin", message.getSender(), "/hist", msg, new Date()));
                                }
                                break;
                            }
                            case "/chroom": {
                                Message answer;
                                if (sockets.moveClientToAnotherRoom(socketConnection, message.getRoom(), message.getMessage())) {
                                    answer = new Message("Admin", message.getSender(), "/chroom", "success", new Date());
                                    answer.setRoom(message.getMessage());
                                } else {
                                    answer = new Message("Admin", message.getSender(), "/chroom", "fail", new Date());
                                    answer.setRoom(message.getRoom());
                                }
                                socketConnection.write(answer);
                                break;
                            }
                        }
                    } catch (SocketException e) {
                        FileConnection.error_write("Socket was closed. Remove socket.");
                        removeSocket(socketConnection);
                        break;

                    } catch (ClassNotFoundException | IOException e) {
                        FileConnection.error_write("Read was failed. Remove socket.");
                        removeSocket(socketConnection);

                        break;
                    }
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }).start();
    }

    public Message readSocket(SocketConnection socketConnection) throws IOException, ClassNotFoundException {
        return (Message) socketConnection.read();
    }

    public boolean addSocket(ClientSocket socketConnection, Message message) throws IOException {
        boolean result = false;
        synchronized (sockets) {
            //System.out.println("try to add");
            socketConnection.setLogin(message.getMessage());
            message.setRecipient(message.getMessage());
            message.setSender("Admin");
            message.setDate(new Date());
            try {
                if (sockets.addClientToDefaultRoom(socketConnection)) {
                    message.setMessage("success");
                    result = true;
                } else {
                    message.setMessage("fail");
                    socketConnection.setLogin(message.getSender().equals("Anonymous") ? "" : message.getSender());
                }
            } catch (Exception e) {
                FileConnection.error_write("Add client to socket failed");
            }
            socketConnection.write(message);
            //System.out.println("client answer: " + answer);
        }
        return result;
    }

    public void removeSocket(ClientSocket socketConnection) throws IOException {
        synchronized (sockets) {
            try {
                if (sockets.removeClientfromPool(socketConnection)) {
                    synchronized (threadCountMonitor) {
                        threadCount--;
                    }
                }
            } catch (IOException e) {
                FileConnection.error_write("Socket closing is failed");
            }
        }
    }

    public void sendMessageToAllClients() throws IOException, InterruptedException {
        Message message;
        message = messageQueue.remove();
        FileConnection.log_write(message.toString());
        while (message != null) {
            synchronized (sockets) {
                for (ClientSocket socket : sockets.getRoom(message.getRoom())) {
                    try {
                        socket.write(message);
                    } catch (IOException e) {
                        try {
                            socket.close();
                            sockets.removeClientfromPool(socket);
                        } catch (IOException e1) {
                            FileConnection.error_write("Error in sending message: " + e1.getMessage());
                        }
                    }
                }
            }
            Thread.sleep(1);
            message = messageQueue.remove();
        }
    }

    @Override
    public void close() throws Exception {
        messageQueue.close();
        serverSocket.close();
    }
}
